Skip to content

feat(ui): add Mosaic Item component#9234

Open
alexcarpenter wants to merge 2 commits into
mainfrom
carp/mosaic-item
Open

feat(ui): add Mosaic Item component#9234
alexcarpenter wants to merge 2 commits into
mainfrom
carp/mosaic-item

Conversation

@alexcarpenter

Copy link
Copy Markdown
Member

Summary

Adds a new Item component to Mosaic, styled with StyleX following the Button migration (themeProps + stylex.props + mergeProps, public contract of .cl-* class + data-* axes). The API matches shadcn's Item, composed via dot syntax.

Item is a row for lists of accounts, organizations, and settings (org switcher, account switcher, settings rows).

Parts

  • Item (root) — variant (default | outline | muted), size (default | sm)
  • Item.Mediavariant (default | icon | image)
  • Item.Content, Item.Title, Item.Description, Item.Actions, Item.Footer
  • Item.Group (role="list"), Item.Separator

Notes

  • Polymorphism uses the repo's render prop (via renderElement), not shadcn's asChild. The root's render is typed generically (like Box) so a row can render as a link or button.
  • Interactive hover is derived from render being present (reflected as data-interactive), replacing shadcn's [a]:hover selector: a plain <Item> div is static; a rendered link/button row gets cursor + muted hover.
  • Styles are split into per-slot stylex.create blocks.
  • No Item.Header (dropped per review); Item.Footer retained.
  • Mosaic has no accent token, so hover/muted use --cl-color-muted via color-mix.

Docs

Swingset docs added (item.stories.tsx + item.mdx, Archetype A·compound) and wired into registry.ts / DocsViewer.tsx.

Testing

  • pnpm vitest run (ui) — 8/8 pass
  • tsc --noEmit clean (ui + swingset)
  • eslint clean (all 9 @stylexjs rules)
  • pnpm format clean

@changeset-bot

changeset-bot Bot commented Jul 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1ac76ed

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jul 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Jul 24, 2026 12:00am
swingset Ready Ready Preview, Comment Jul 24, 2026 12:00am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a composable Mosaic Item component with typed subcomponents, StyleX styling, polymorphic rendering, ref forwarding, and tests. Exports the component through Mosaic barrels, adds Storybook stories and MDX documentation, registers the stories and docs in Swingset, and includes a changeset entry.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • clerk/javascript#9184: Updates the same Swingset MDX and story registry wiring paths for another component.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: adding the Mosaic Item component.
Description check ✅ Passed The description is directly related to the changeset and accurately summarizes the new component, docs, and testing.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
packages/ui/src/mosaic/components/item/index.ts (1)

1-2: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid introducing the Item re-export barrel.

The new directory index.ts adds an unnecessary re-export boundary and the public entry consumes it. Export directly from item.tsx instead to avoid a future circular-dependency edge.

  • packages/ui/src/mosaic/components/item/index.ts#L1-L2: remove the local re-export barrel.
  • packages/ui/src/mosaic/styles/index.ts#L10-L11: re-export from ../components/item/item directly.

As per coding guidelines, “Avoid barrel files (index.ts re-exports) as they can cause circular dependencies.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/mosaic/components/item/index.ts` around lines 1 - 2, Remove
the Item re-export barrel from packages/ui/src/mosaic/components/item/index.ts
lines 1-2. Update packages/ui/src/mosaic/styles/index.ts lines 10-11 to
re-export Item, ItemMediaProps, and ItemProps directly from
../components/item/item, preserving the existing public exports.

Source: Coding guidelines

packages/swingset/src/stories/item.stories.tsx (1)

17-49: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add explicit return types to the story components.

Annotate BuildingIcon, ArrowIcon, and the exported stories with the project’s JSX element return type instead of relying on inference.

As per coding guidelines, “Always define explicit return types for functions, especially public APIs.”

Also applies to: 51-153

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swingset/src/stories/item.stories.tsx` around lines 17 - 49,
Annotate BuildingIcon, ArrowIcon, and every exported story component in this
file with the project’s established JSX element return type. Apply the same
explicit return-type convention consistently across the story definitions,
including the stories referenced in the additional range, without changing their
rendering behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/mosaic-item.md:
- Around line 1-2: Add a non-empty changeset for the new public `Item` export
from `@clerk/ui`, specifying the appropriate package bump and a concise release
note describing the API addition. Replace the current empty frontmatter in
`.changeset/mosaic-item.md` while preserving valid changeset formatting.

In `@packages/swingset/src/stories/item.mdx`:
- Around line 37-53: Add the required public Button import and provide a
BuildingIcon import or local definition in the Item usage example so the snippet
is self-contained and compiles when copied; preserve the existing Item.Group
structure and usage.

In `@packages/ui/src/mosaic/components/item/item.tsx`:
- Around line 149-151: Remove the role="list" assignment from Item.Group in
packages/ui/src/mosaic/components/item/item.tsx (lines 149-151), since it cannot
guarantee listitem children; update the corresponding semantic-role assertion in
packages/ui/src/mosaic/components/item/item.test.tsx (lines 91-101), and remove
or revise the role="list" documentation claim in
packages/swingset/src/stories/item.mdx (line 67).
- Around line 16-168: Integrate the Item surface with the customization contract
by defining stable element descriptors for Root and each slot component: Media,
Content, Title, Description, Actions, Footer, Group, and Separator. Pass the
appropriate descriptor through each renderElement call and wrap the exported
primitive components with the repository’s makeCustomizable HOC so appearance
elements overrides can target every Item element.

---

Nitpick comments:
In `@packages/swingset/src/stories/item.stories.tsx`:
- Around line 17-49: Annotate BuildingIcon, ArrowIcon, and every exported story
component in this file with the project’s established JSX element return type.
Apply the same explicit return-type convention consistently across the story
definitions, including the stories referenced in the additional range, without
changing their rendering behavior.

In `@packages/ui/src/mosaic/components/item/index.ts`:
- Around line 1-2: Remove the Item re-export barrel from
packages/ui/src/mosaic/components/item/index.ts lines 1-2. Update
packages/ui/src/mosaic/styles/index.ts lines 10-11 to re-export Item,
ItemMediaProps, and ItemProps directly from ../components/item/item, preserving
the existing public exports.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 41e7b2f0-ab0b-49bc-bdbf-bed0f2e18d58

📥 Commits

Reviewing files that changed from the base of the PR and between 17d1db1 and d37de13.

📒 Files selected for processing (10)
  • .changeset/mosaic-item.md
  • packages/swingset/src/components/DocsViewer.tsx
  • packages/swingset/src/lib/registry.ts
  • packages/swingset/src/stories/item.mdx
  • packages/swingset/src/stories/item.stories.tsx
  • packages/ui/src/mosaic/components/item/index.ts
  • packages/ui/src/mosaic/components/item/item.styles.ts
  • packages/ui/src/mosaic/components/item/item.test.tsx
  • packages/ui/src/mosaic/components/item/item.tsx
  • packages/ui/src/mosaic/styles/index.ts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • clerk/clerk_go (manual)
  • clerk/dashboard (manual)
  • clerk/accounts (manual)
  • clerk/backoffice (manual)
  • clerk/clerk (manual)
  • clerk/clerk-docs (manual)
  • clerk/cloudflare-workers (manual)
  • clerk/clerk-ios (auto-detected)
  • clerk/cli (auto-detected)
  • clerk/clerk-android (auto-detected)

Comment thread .changeset/mosaic-item.md
Comment on lines +1 to +2
---
---

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Add a release entry for the new public UI API.

This PR exports Item from @clerk/ui; an empty changeset risks shipping no version containing the component. Add the appropriate @clerk/ui bump and release note.

Based on learnings, empty changesets are acceptable for documentation-only or internal-only work, not a published public API addition.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.changeset/mosaic-item.md around lines 1 - 2, Add a non-empty changeset for
the new public `Item` export from `@clerk/ui`, specifying the appropriate
package bump and a concise release note describing the API addition. Replace the
current empty frontmatter in `.changeset/mosaic-item.md` while preserving valid
changeset formatting.

Sources: Coding guidelines, Learnings

Comment on lines +37 to +53
```tsx
import { Item } from '@clerk/ui/mosaic/components/item';

<Item.Group>
<Item render={({ children, ...props }) => <a {...props} href='/org'>{children}</a>}>
<Item.Media variant='icon'>
<BuildingIcon />
</Item.Media>
<Item.Content>
<Item.Title>Test Organization</Item.Title>
<Item.Description>Member</Item.Description>
</Item.Content>
<Item.Actions>
<Button variant='outline'>Manage</Button>
</Item.Actions>
</Item>
</Item.Group>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the usage example self-contained.

Button and BuildingIcon are used but never imported or defined, so this snippet does not compile when copied. Add the public Button import and an icon import/definition, or remove those usages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swingset/src/stories/item.mdx` around lines 37 - 53, Add the
required public Button import and provide a BuildingIcon import or local
definition in the Item usage example so the snippet is self-contained and
compiles when copied; preserve the existing Item.Group structure and usage.

Source: Coding guidelines

Comment on lines +16 to +168
const Root = React.forwardRef<HTMLDivElement, ItemProps>(function MosaicItem(
{ variant = 'default', size = 'default', render, className, style, ...rest },
ref,
) {
// A custom render (link/button row) opts into hover + cursor affordances.
const interactive = Boolean(render);
return renderElement({
defaultTagName: 'div',
render,
props: {
ref,
...mergeProps(
themeProps('item', { variant, size, interactive }),
stylex.props(
slots.item.base,
variant === 'outline' && slots.item.variantOutline,
variant === 'muted' && slots.item.variantMuted,
variant === 'default' && slots.item.variantDefault,
size === 'sm' ? slots.item.sizeSm : slots.item.sizeMd,
interactive && slots.item.interactive,
),
className,
style,
),
...rest,
},
});
});

export type ItemMediaProps = ComponentProps<'div'> & {
variant?: 'default' | 'icon' | 'image';
};

const Media = React.forwardRef<HTMLDivElement, ItemMediaProps>(function MosaicItemMedia(
{ variant = 'default', render, className, style, ...rest },
ref,
) {
return renderElement({
defaultTagName: 'div',
render,
props: {
ref,
...mergeProps(
themeProps('item-media', { variant }),
stylex.props(
slots.media.base,
variant === 'icon' && slots.media.icon,
variant === 'image' && slots.media.image,
),
className,
style,
),
...rest,
},
});
});

const Content = React.forwardRef<HTMLDivElement, ComponentProps<'div'>>(function MosaicItemContent(
{ render, className, style, ...rest },
ref,
) {
return renderElement({
defaultTagName: 'div',
render,
props: {
ref,
...mergeProps(themeProps('item-content'), stylex.props(slots.content.base), className, style),
...rest,
},
});
});

const Title = React.forwardRef<HTMLDivElement, ComponentProps<'div'>>(function MosaicItemTitle(
{ render, className, style, ...rest },
ref,
) {
return renderElement({
defaultTagName: 'div',
render,
props: { ref, ...mergeProps(themeProps('item-title'), stylex.props(slots.title.base), className, style), ...rest },
});
});

const Description = React.forwardRef<HTMLParagraphElement, ComponentProps<'p'>>(function MosaicItemDescription(
{ render, className, style, ...rest },
ref,
) {
return renderElement({
defaultTagName: 'p',
render,
props: {
ref,
...mergeProps(themeProps('item-description'), stylex.props(slots.description.base), className, style),
...rest,
},
});
});

const Actions = React.forwardRef<HTMLDivElement, ComponentProps<'div'>>(function MosaicItemActions(
{ render, className, style, ...rest },
ref,
) {
return renderElement({
defaultTagName: 'div',
render,
props: {
ref,
...mergeProps(themeProps('item-actions'), stylex.props(slots.actions.base), className, style),
...rest,
},
});
});

const Footer = React.forwardRef<HTMLDivElement, ComponentProps<'div'>>(function MosaicItemFooter(
{ render, className, style, ...rest },
ref,
) {
return renderElement({
defaultTagName: 'div',
render,
props: { ref, ...mergeProps(themeProps('item-footer'), stylex.props(slots.band.base), className, style), ...rest },
});
});

const Group = React.forwardRef<HTMLDivElement, ComponentProps<'div'>>(function MosaicItemGroup(
{ render, className, style, ...rest },
ref,
) {
return renderElement({
defaultTagName: 'div',
render,
props: {
ref,
role: 'list',
...mergeProps(themeProps('item-group'), stylex.props(slots.group.base), className, style),
...rest,
},
});
});

const Separator = React.forwardRef<HTMLHRElement, ComponentProps<'hr'>>(function MosaicItemSeparator(
{ render, className, style, ...rest },
ref,
) {
return renderElement({
defaultTagName: 'hr',
render,
props: {
ref,
...mergeProps(themeProps('item-separator'), stylex.props(slots.separator.base), className, style),
...rest,
},
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Wire Item slots into the UI customization contract.

The new themable Item surface only emits themeProps/StyleX classes; no slot receives an elementDescriptor or is wrapped through the customizable primitive path. Appearance elements overrides therefore cannot target this public component consistently.

Add stable descriptors for the root and slots, then apply the repository’s elementDescriptor/makeCustomizable integration.

As per coding guidelines, “Use element descriptors for all themable elements” and “Wrap primitive components with makeCustomizable HOC.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/mosaic/components/item/item.tsx` around lines 16 - 168,
Integrate the Item surface with the customization contract by defining stable
element descriptors for Root and each slot component: Media, Content, Title,
Description, Actions, Footer, Group, and Separator. Pass the appropriate
descriptor through each renderElement call and wrap the exported primitive
components with the repository’s makeCustomizable HOC so appearance elements
overrides can target every Item element.

Source: Coding guidelines

Comment on lines +149 to +151
role: 'list',
...mergeProps(themeProps('item-group'), stylex.props(slots.group.base), className, style),
...rest,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not expose a list role without list-item children.

Item.Group declares an ARIA list, but its direct Item and Item.Separator children are div/hr or arbitrary render targets, not listitems. Either remove the list role or redesign the group API to enforce semantic list items.

  • packages/ui/src/mosaic/components/item/item.tsx#L149-L151: remove role: 'list' unless the component can guarantee listitem children.
  • packages/ui/src/mosaic/components/item/item.test.tsx#L91-L101: update the test to reflect the corrected semantic contract.
  • packages/swingset/src/stories/item.mdx#L67-L67: remove or revise the role="list" documentation claim.

As per coding guidelines, “Use proper HTML elements for semantic HTML in React components.”

📍 Affects 3 files
  • packages/ui/src/mosaic/components/item/item.tsx#L149-L151 (this comment)
  • packages/ui/src/mosaic/components/item/item.test.tsx#L91-L101
  • packages/swingset/src/stories/item.mdx#L67-L67
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/mosaic/components/item/item.tsx` around lines 149 - 151,
Remove the role="list" assignment from Item.Group in
packages/ui/src/mosaic/components/item/item.tsx (lines 149-151), since it cannot
guarantee listitem children; update the corresponding semantic-role assertion in
packages/ui/src/mosaic/components/item/item.test.tsx (lines 91-101), and remove
or revise the role="list" documentation claim in
packages/swingset/src/stories/item.mdx (line 67).

Source: Coding guidelines

@pkg-pr-new

pkg-pr-new Bot commented Jul 23, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@9234

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@9234

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@9234

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@9234

@clerk/electron

npm i https://pkg.pr.new/@clerk/electron@9234

@clerk/electron-passkeys

npm i https://pkg.pr.new/@clerk/electron-passkeys@9234

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@9234

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@9234

@clerk/expo-google-signin

npm i https://pkg.pr.new/@clerk/expo-google-signin@9234

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@9234

@clerk/express

npm i https://pkg.pr.new/@clerk/express@9234

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@9234

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@9234

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@9234

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@9234

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@9234

@clerk/react

npm i https://pkg.pr.new/@clerk/react@9234

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@9234

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@9234

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@9234

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@9234

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@9234

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@9234

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@9234

commit: 1ac76ed

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-07-24T00:01:10.426Z

Summary

Metric Count
Packages analyzed 19
Packages with changes 0
🔴 Breaking changes 0
🟡 Non-breaking changes 0
🟢 Additions 0

No API Changes Detected

All packages have stable APIs with no detected changes.


Report generated by Break Check

Last ran on 1ac76ed.

The Mosaic StyleX build resolved @clerk/headless types against its published
dist .d.ts re-export barrels, which rolldown-plugin-dts can't follow when
inlining a bundled dependency. Since @clerk/headless is private and must be
inlined, map it to source in tsconfig.mosaic.json (the monorepo default, same
pattern already used for @/core/*), so Mosaic components can import headless
types directly.
Add a Mosaic `Item` component for lists of accounts, organizations, and
settings rows. Composed via dot syntax (`Item.Media`, `Item.Content`,
`Item.Title`, `Item.Description`, `Item.Actions`, `Item.Footer`,
`Item.Group`, `Item.Separator`) with `default`/`outline`/`muted` variants and
`default`/`sm` sizes. Per-slot StyleX styles compile into the shared sheet, and
a `render` prop allows an item to render as a link or button. Includes swingset
docs and stories.
@alexcarpenter alexcarpenter changed the title feat(ui): add Mosaic Item component with StyleX styling feat(ui): add Mosaic Item component Jul 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant